﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using NuGet.Versioning;

namespace Microsoft.NET.Build.Tasks
{
    public class ResolveReadyToRunCompilers : TaskBase
    {
        public bool EmitSymbols { get; set; }
        public bool ReadyToRunUseCrossgen2 { get; set; }
        public string PerfmapFormatVersion { get; set; }

        [Required]
        public ITaskItem[] RuntimePacks { get; set; }
        public ITaskItem[] Crossgen2Packs { get; set; }
        [Required]
        public ITaskItem[] TargetingPacks { get; set; }
        [Required]
        public string RuntimeGraphPath { get; set; }
        [Required]
        public string NETCoreSdkRuntimeIdentifier { get; set; }

        [Output]
        public ITaskItem CrossgenTool { get; set; }
        [Output]
        public ITaskItem Crossgen2Tool { get; set; }

        internal struct CrossgenToolInfo
        {
            public string ToolPath;
            public string PackagePath;
            public string ClrJitPath;
            public string DiaSymReaderPath;
        }

        private ITaskItem _runtimePack;
        private string _targetRuntimeIdentifier;
        private string _targetPlatform;
        private string _hostRuntimeIdentifier;

        private CrossgenToolInfo _crossgenTool;
        private CrossgenToolInfo _crossgen2Tool;

        private Architecture _targetArchitecture;
        private bool _crossgen2IsVersion5;

        protected override void ExecuteCore()
        {
            _runtimePack = GetNETCoreAppRuntimePack();
            _targetRuntimeIdentifier = _runtimePack?.GetMetadata(MetadataKeys.RuntimeIdentifier);

            if (_targetRuntimeIdentifier == null)
            {
                Log.LogError(Strings.ReadyToRunNoValidRuntimePackageError);
                return;
            }

            if (ReadyToRunUseCrossgen2)
            {
                if (!ValidateCrossgen2Support())
                {
                    return;
                }

                // In .NET 5 Crossgen2 did not support emitting native symbols, so we use Crossgen to emit them
                if (_crossgen2IsVersion5 && EmitSymbols && !ValidateCrossgenSupport())
                {
                    return;
                }
            }
            else
            {
                if (!ValidateCrossgenSupport())
                {
                    return;
                }
            }
        }

        private bool ValidateCrossgenSupport()
        {
            _hostRuntimeIdentifier = GetHostRuntimeIdentifierForCrossgen();
            if (_hostRuntimeIdentifier == null)
            {
                Log.LogError(Strings.ReadyToRunNoValidRuntimePackageError);
                return false;
            }

            _crossgenTool.PackagePath = _runtimePack?.GetMetadata(MetadataKeys.PackageDirectory);
            if (_crossgenTool.PackagePath == null)
            {
                Log.LogError(Strings.ReadyToRunNoValidRuntimePackageError);
                return false;
            }

            if (!ExtractTargetPlatformAndArchitecture(_targetRuntimeIdentifier, out _targetPlatform, out _targetArchitecture) ||
                !ExtractTargetPlatformAndArchitecture(_hostRuntimeIdentifier, out string hostPlatform, out Architecture hostArchitecture) ||
                _targetPlatform != hostPlatform ||
                !GetCrossgenComponentsPaths())
            {
                Log.LogError(Strings.ReadyToRunTargetNotSupportedError);
                return false;
            }

            // Create tool task item
            CrossgenTool = new TaskItem(_crossgenTool.ToolPath);
            CrossgenTool.SetMetadata(MetadataKeys.JitPath, _crossgenTool.ClrJitPath);
            if (!string.IsNullOrEmpty(_crossgenTool.DiaSymReaderPath))
            {
                CrossgenTool.SetMetadata(MetadataKeys.DiaSymReader, _crossgenTool.DiaSymReaderPath);
            }

            return true;

            string GetHostRuntimeIdentifierForCrossgen()
            {
                // Crossgen's host RID comes from the runtime pack that Crossgen will be loaded from.

                // Get the list of runtime identifiers that we support and can target
                ITaskItem targetingPack = GetNETCoreAppTargetingPack();
                string supportedRuntimeIdentifiers = targetingPack?.GetMetadata(MetadataKeys.RuntimePackRuntimeIdentifiers);

                var runtimeGraph = new RuntimeGraphCache(this).GetRuntimeGraph(RuntimeGraphPath);
                var supportedRIDsList = supportedRuntimeIdentifiers == null ? Array.Empty<string>() : supportedRuntimeIdentifiers.Split(';');

                // Get the best RID for the host machine, which will be used to validate that we can run crossgen for the target platform and architecture
                return NuGetUtils.GetBestMatchingRid(
                    runtimeGraph,
                    NETCoreSdkRuntimeIdentifier,
                    supportedRIDsList,
                    out _);
            }
        }

        private bool ValidateCrossgen2Support()
        {
            ITaskItem crossgen2Pack = Crossgen2Packs?.FirstOrDefault();

            _hostRuntimeIdentifier = crossgen2Pack?.GetMetadata(MetadataKeys.RuntimeIdentifier);
            if (_hostRuntimeIdentifier == null)
            {
                Log.LogError(Strings.ReadyToRunNoValidRuntimePackageError);
                return false;
            }

            _crossgen2Tool.PackagePath = crossgen2Pack.GetMetadata(MetadataKeys.PackageDirectory);

            if (string.IsNullOrEmpty(_crossgen2Tool.PackagePath) ||
                !NuGetVersion.TryParse(crossgen2Pack.GetMetadata(MetadataKeys.NuGetPackageVersion), out NuGetVersion crossgen2PackVersion))
            {
                Log.LogError(Strings.ReadyToRunNoValidRuntimePackageError);
                return false;
            }

            bool version5 = crossgen2PackVersion.Major < 6;
            bool isSupportedTarget = ExtractTargetPlatformAndArchitecture(_targetRuntimeIdentifier, out _targetPlatform, out _targetArchitecture);

            // In .NET 5 Crossgen2 supported only the following host->target compilation scenarios:
            //      win-x64 -> win-x64
            //      linux-x64 -> linux-x64
            //      linux-musl-x64 -> linux-musl-x64
            string targetOS = null;
            isSupportedTarget = isSupportedTarget &&
                GetCrossgen2TargetOS(out targetOS) &&
                (!version5 || _targetRuntimeIdentifier == _hostRuntimeIdentifier) &&
                GetCrossgen2ComponentsPaths(version5);

            if (!isSupportedTarget)
            {
                Log.LogError(Strings.ReadyToRunTargetNotSupportedError);
                return false;
            }

            // Create tool task item
            Crossgen2Tool = new TaskItem(_crossgen2Tool.ToolPath);
            Crossgen2Tool.SetMetadata(MetadataKeys.IsVersion5, version5.ToString());
            if (version5)
            {
                Crossgen2Tool.SetMetadata(MetadataKeys.JitPath, _crossgen2Tool.ClrJitPath);
            }
            else
            {
                Crossgen2Tool.SetMetadata(MetadataKeys.TargetOS, targetOS);
                Crossgen2Tool.SetMetadata(MetadataKeys.TargetArch, ArchitectureToString(_targetArchitecture));
                if (!string.IsNullOrEmpty(PerfmapFormatVersion))
                {
                    Crossgen2Tool.SetMetadata(MetadataKeys.PerfmapFormatVersion, PerfmapFormatVersion);
                }
            }

            _crossgen2IsVersion5 = version5;
            return true;
        }

        private bool GetCrossgen2TargetOS(out string targetOS)
        {
            targetOS = null;

            // Determine targetOS based on target rid.
            // Use the runtime graph to support non-portable target rids.
            // Use the full target rid instead of just the target OS as the runtime graph
            // may only have the full target rid and not an OS-only rid for non-portable target rids
            // added by our source-build partners.
            var runtimeGraph = new RuntimeGraphCache(this).GetRuntimeGraph(RuntimeGraphPath);
            string portablePlatform = NuGetUtils.GetBestMatchingRid(
                    runtimeGraph,
                    _targetRuntimeIdentifier,
                    ["linux", "osx", "win", "freebsd", "illumos", "haiku"],
                    out _);

            targetOS = portablePlatform switch
            {
                "linux" => "linux",
                "osx" => "osx",
                "win" => "windows",
                "freebsd" => "freebsd",
                "illumos" => "illumos",
                "haiku" => "haiku",
                _ => null
            };

            return targetOS != null;
        }

        private ITaskItem GetNETCoreAppRuntimePack()
        {
            return GetNETCoreAppPack(RuntimePacks, MetadataKeys.FrameworkName);
        }

        private ITaskItem GetNETCoreAppTargetingPack()
        {
            return GetNETCoreAppPack(TargetingPacks, MetadataKeys.RuntimeFrameworkName);
        }

        private static ITaskItem GetNETCoreAppPack(ITaskItem[] packs, string metadataKey)
        {
            return packs.SingleOrDefault(
                pack => pack.GetMetadata(metadataKey)
                            .Equals("Microsoft.NETCore.App", StringComparison.OrdinalIgnoreCase));
        }

        private static bool ExtractTargetPlatformAndArchitecture(string runtimeIdentifier, out string platform, out Architecture architecture)
        {
            platform = null;
            architecture = default;

            // This will split RID like "linux-musl-arm64" into "linux-musl" and "arm64" components
            int separator = runtimeIdentifier.LastIndexOf('-');
            if (separator < 0)
            {
                return false;
            }

            string architectureStr = runtimeIdentifier.Substring(separator + 1).ToLowerInvariant();

            switch (architectureStr)
            {
                case "arm":
                    architecture = Architecture.Arm;
                    break;
                case "arm64":
                    architecture = Architecture.Arm64;
                    break;
#if !NETFRAMEWORK
                case "riscv64":
                    architecture = Architecture.RiscV64;
                    break;
                case "loongarch64":
                    architecture = Architecture.LoongArch64;
                    break;
#endif
                case "x64":
                    architecture = Architecture.X64;
                    break;
                case "x86":
                    architecture = Architecture.X86;
                    break;
                default:
                    return false;
            }

            platform = runtimeIdentifier.Substring(0, separator).ToLowerInvariant();
            return true;
        }

        private bool GetCrossgenComponentsPaths()
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                if (_targetArchitecture == Architecture.Arm)
                {
                    if (RuntimeInformation.OSArchitecture == _targetArchitecture)
                    {
                        // We can run native arm32 bits on an arm64 host in WOW mode
                        _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "crossgen.exe");
                        _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "clrjit.dll");
                        _crossgenTool.DiaSymReaderPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "Microsoft.DiaSymReader.Native.arm.dll");
                    }
                    else
                    {
                        // We can use the x86-hosted crossgen compiler to target ARM
                        _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "x86_arm", "crossgen.exe");
                        _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", "x86_arm", "native", "clrjit.dll");
                        _crossgenTool.DiaSymReaderPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", "x86_arm", "native", "Microsoft.DiaSymReader.Native.x86.dll");
                    }
                }
                else if (_targetArchitecture == Architecture.Arm64)
                {
                    if (RuntimeInformation.OSArchitecture == _targetArchitecture)
                    {
                        _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "crossgen.exe");
                        _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "clrjit.dll");
                        _crossgenTool.DiaSymReaderPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "Microsoft.DiaSymReader.Native.arm64.dll");
                    }
                    else
                    {
                        // We only have 64-bit hosted compilers for ARM64.
                        if (RuntimeInformation.OSArchitecture != Architecture.X64)
                        {
                            return false;
                        }

                        _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "x64_arm64", "crossgen.exe");
                        _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", "x64_arm64", "native", "clrjit.dll");
                        _crossgenTool.DiaSymReaderPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", "x64_arm64", "native", "Microsoft.DiaSymReader.Native.amd64.dll");
                    }
                }
                else
                {
                    _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "crossgen.exe");
                    _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "clrjit.dll");
                    if (_targetArchitecture == Architecture.X64)
                    {
                        _crossgenTool.DiaSymReaderPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "Microsoft.DiaSymReader.Native.amd64.dll");
                    }
                    else
                    {
                        _crossgenTool.DiaSymReaderPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "Microsoft.DiaSymReader.Native.x86.dll");
                    }
                }
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                if (_targetArchitecture == Architecture.Arm || _targetArchitecture == Architecture.Arm64)
                {
                    if (RuntimeInformation.OSArchitecture == _targetArchitecture)
                    {
                        _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "crossgen");
                        _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "libclrjit.so");
                    }
                    else if (RuntimeInformation.OSArchitecture == Architecture.X64)
                    {
                        string xarchPath = (_targetArchitecture == Architecture.Arm ? "x64_arm" : "x64_arm64");
                        _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", xarchPath, "crossgen");
                        _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", xarchPath, "native", "libclrjit.so");
                    }
                    else
                    {
                        return false;
                    }
                }
                else
                {
                    _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "crossgen");
                    _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "libclrjit.so");
                }
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                // Only x64 supported for OSX
                if (_targetArchitecture != Architecture.X64 || RuntimeInformation.OSArchitecture != Architecture.X64)
                {
                    return false;
                }

                _crossgenTool.ToolPath = Path.Combine(_crossgenTool.PackagePath, "tools", "crossgen");
                _crossgenTool.ClrJitPath = Path.Combine(_crossgenTool.PackagePath, "runtimes", _targetRuntimeIdentifier, "native", "libclrjit.dylib");
            }
            else
            {
                // Unknown platform
                return false;
            }

            return File.Exists(_crossgenTool.ToolPath) && File.Exists(_crossgenTool.ClrJitPath);
        }

        private bool GetCrossgen2ComponentsPaths(bool version5)
        {
            string toolFileName, v5_clrJitFileNamePattern;

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                toolFileName = "crossgen2.exe";
                v5_clrJitFileNamePattern = "clrjit-{0}.dll";
            }
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                toolFileName = "crossgen2";
                v5_clrJitFileNamePattern = "libclrjit-{0}.dylib";
            }
            else
            {
                // Generic Unix-like: linux, freebsd, and others.
                toolFileName = "crossgen2";
                v5_clrJitFileNamePattern = "libclrjit-{0}.so";
            }

            if (version5)
            {
                string clrJitFileName = string.Format(v5_clrJitFileNamePattern, GetTargetSpecForVersion5());
                _crossgen2Tool.ClrJitPath = Path.Combine(_crossgen2Tool.PackagePath, "tools", clrJitFileName);
                if (!File.Exists(_crossgen2Tool.ClrJitPath))
                {
                    return false;
                }
            }

            _crossgen2Tool.ToolPath = Path.Combine(_crossgen2Tool.PackagePath, "tools", toolFileName);
            return File.Exists(_crossgen2Tool.ToolPath);
        }

        // Keep in sync with JitConfigProvider.GetTargetSpec in .NET 5
        private string GetTargetSpecForVersion5()
        {
            string targetOSComponent = (_targetPlatform == "win" ? "win" : "unix");
            string targetArchComponent = ArchitectureToString(_targetArchitecture);
            return targetOSComponent + '-' + targetArchComponent;
        }

        private static string ArchitectureToString(Architecture architecture)
        {
            return architecture switch
            {
                Architecture.X86 => "x86",
                Architecture.X64 => "x64",
                Architecture.Arm => "arm",
                Architecture.Arm64 => "arm64",
#if !NETFRAMEWORK
                Architecture.RiscV64 => "riscv64",
                Architecture.LoongArch64 => "loongarch64",
#endif
                _ => null
            };
        }
    }
}
